package net.chowda.castcluster.playlist;

import net.chowda.castcluster.PlayList;
import net.chowda.castcluster.Video;
import net.chowda.castcluster.VideoProvider;
import net.chowda.castcluster.util.CastClusterConfig;
import net.chowda.castcluster.util.LogUtil;
import net.chowda.castcluster.util.RSSFeedAggregator;
import org.apache.log4j.Logger;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.jdom.xpath.XPath;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.util.*;

/**
 * User: dbachelder
 *
 * this class keeps all the playlists in memory and modifies, deletes, edits, creates and saves them all to the XML
 * file when used.
 * <p/>
 * this class is a bit of a mishmash
 * TODO - unmishmash
 */
public class PlayListManager {
    private static final Logger LOG = LogUtil.getLogger(PlayListManager.class);
    // we maintain the Map and the list.. they should always contain the same objects. the map is keyed by playlist id.
    private static final Map<String, PlayList> playListMap = new HashMap<String, PlayList>();
    private static final List<PlayList> playLists = new ArrayList<PlayList>();

    // we load the XML once when the class is loaded, from then on, the objects in memory are the golden source
    static {
        try {
            playLists.addAll(loadListsFromDOM());
            for (PlayList list : playLists) {
                playListMap.put(list.getId(), list);
            }
        } catch (Exception e) {
            LOG.error(e);
            throw new RuntimeException(e);
        }
    }

    public static List<PlayList> getAllPlayLists() throws Exception {
        return playLists;
    }

    public static PlayList getPlayList(String listId) throws Exception {
        return playListMap.get(listId);
    }

    /**
     * @return XML string representing playlist..
     *          can be turned back into playlist with xmlToPlaylist()
     */
    public static String playListToXML(String playListId) throws Exception {
        XPath xPath = XPath.newInstance("//playlist[@id='" + playListId + "']");
        Element playListElement = (Element) xPath.selectSingleNode(getPlayListDOM());
        XMLOutputter xmlOutputter = new XMLOutputter();
        xmlOutputter.setFormat(Format.getPrettyFormat());
        return xmlOutputter.outputString(playListElement);
    }

    /**
     * @return list of all the feeds.. one for each source in the playlist.
     */
    public static List<String> getFeedsForList(PlayList list) {
        List<String> feeds = new ArrayList<String>(20);
        if (list != null) {
            for (String item : list.getUrls()) {
                try {
                    feeds.add(VideoProvider.getFeed(item));
                } catch (Exception e) {
                    try {
                        // sometimes the feed will be so malformed we can't work with it. so we'll take a shot at parsing it into videos.
                        feeds.add(RSSFeedAggregator.createPodcastFromVideos(list.getName(), list.getDescription(), VideoProvider.getVideos(item)));
                    } catch (Exception e2) {
                        LOG.error("tried to get feed both ways, no good (first exception): " + item, e);
                        LOG.error("tried to get feed both ways, no good (second exception): " + item, e2);
                    }
                }
            }
        }
        return feeds;
    }

    /**
     * convenience method... get the videos for each source from it's provider, then sort them all by upload time.
     */
    public static List<Video> getVideosForList(PlayList list) throws Exception {
        List<Video> allVideos = new ArrayList<Video>(100);

        if (list != null) {
            for (String item : list.getUrls()) {
                allVideos.addAll(VideoProvider.getVideos(item));
            }

            Collections.sort(allVideos, new Comparator<Video>() {
                public int compare(Video o1, Video o2) {
                    if(o2.getTimeUploaded() == o1.getTimeUploaded()) return 0;
                    if(o2.getTimeUploaded() == null) return -1;
                    if(o1.getTimeUploaded() == null) return 1;
                    return o2.getTimeUploaded().compareTo(o1.getTimeUploaded());
                }
            });
        }
        return allVideos;
    }

    public synchronized static void removeUrlFromPlayList(String listId, String videoUrl) throws Exception {
        PlayList list = getPlayList(listId);
        list.removeUrl(videoUrl);
        savePlayListsToDOM();
    }

    public synchronized static void addUrlToPlayList(String listId, String videoUrl) throws Exception {
        PlayList list = getPlayList(listId);
        list.addUrl(videoUrl);
        savePlayListsToDOM();
    }

    /**
     * create playlist in the model.. save the model to disk.
     */
    public static void createPlayList(String playListName, String playListDescription) throws Exception {
        PlayList playList = new PlayList();
        playList.setName(playListName);
        playList.setDescription(playListDescription);
        playList.setId(playListIdFromName(playListName));
        createPlayList(playList);
    }

    /**
     * adds a playlist to the model and saves the model to disk.
     */
    public synchronized static void createPlayList(PlayList playList) {
        playListMap.put(playList.getId(), playList);
        playLists.add(playList);
        savePlayListsToDOM();
    }

    /**
     * delete playlist from model and save to disk.
     */
    public synchronized static void deletePlayList(String listId) throws Exception {
        PlayList playList1 = playListMap.remove(listId);
        playLists.remove(playList1);
        savePlayListsToDOM();
    }

    /**
     * walk the list of playlists and overwrite the playlist XML file with what we find.
     */
    private synchronized static void savePlayListsToDOM() {
        try {
            Element root = new Element("playlists");
            for (PlayList playList : playLists) {

                Element plEl = new Element("playlist").setAttribute("id", playList.getId());
                plEl.addContent(new Element("name").addContent(playList.getName()));
                plEl.addContent(new Element("description").addContent(playList.getDescription()));
                Element itemsEl = new Element("items");
                for (String videoUrl : playList.getUrls()) {
                    itemsEl.addContent(new Element("item").addContent(videoUrl));
                }
                plEl.addContent(itemsEl);
                root.addContent(plEl);
            }
            saveDOM(new Document(root));
        } catch (Exception e) {
            LOG.error("could not save document", e);
        }
    }

    /**
     * turns passed in XML into playlist. playlist is NOT added to model.
     */
    public static PlayList xmlToPlaylist(String playListXml) throws JDOMException {
        PlayList list = null;
        try {
            Document document = new SAXBuilder().build(new StringReader(playListXml));
            list = elementToPlaylist(document.getRootElement());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return list;
    }

    private static PlayList elementToPlaylist(Element el) throws JDOMException {
        PlayList list = null;
        if (el != null) {
            list = new PlayList();
            Element name = (Element) XPath.selectSingleNode(el, "name");
            Element desription = (Element) XPath.selectSingleNode(el, "description");
            list.setId(el.getAttribute("id").getValue());
            list.setName(name.getTextTrim());
            list.setDescription(desription.getTextTrim());
            List<Element> items = XPath.selectNodes(el, "items/item");
            for (Element item : items) {
                list.addUrl(item.getTextTrim());
            }
        }
        return list;
    }

    private synchronized static List<PlayList> loadListsFromDOM() throws JDOMException, IOException {
        ArrayList<PlayList> lists = new ArrayList<PlayList>();
        Document document = getPlayListDOM();
        List nodes = XPath.newInstance("//playlist").selectNodes(document);
        for (Object node : nodes) {
            Element el = (Element) node;
            lists.add(elementToPlaylist(el));
        }
        return lists;
    }

    private static Document getPlayListDOM() throws JDOMException, IOException {
        File configFile = getConfigFile();
        return new SAXBuilder().build(configFile);
    }

    private synchronized static File getConfigFile() throws IOException {
        String base = CastClusterConfig.getInstance().getGlobalProp(CastClusterConfig.CONFIG_BASE_DIR);
        new File(base).mkdirs();
        File configFile = new File(base + File.separator + "playlists.xml");
        if(!configFile.exists()) {
            // if the file doesn't exist yet, we'll create it.
            FileWriter fileWriter = new FileWriter(configFile);
            fileWriter.write("<castcluster><playlists></playlists></castcluster>");
            fileWriter.flush();
            fileWriter.close();
        }
        return configFile;
    }

    private synchronized static void saveDOM(Document document) throws Exception {
        XMLOutputter xmlOutputter = new XMLOutputter();
        xmlOutputter.setFormat(Format.getPrettyFormat());
        // TODO -- this is probably impossible to scale... and probably only marginally passable for a single user.
        FileWriter fileWriter = new FileWriter(getConfigFile());
        xmlOutputter.output(document, fileWriter);
        fileWriter.flush();
        fileWriter.close();
    }

    private static String playListIdFromName(String playListName) {
        StringBuilder id = new StringBuilder(playListName.length());
        char[] chars = playListName.toCharArray();
        for (char aChar : chars) {
            id.append(Character.isLetterOrDigit(aChar) ? aChar : '_');
        }
        return id.toString();
    }
}
